﻿// ===============================================================================
//  Copyright © ymind.net. All rights reserved .
//  官方网站：http://ymind.net/
//  版权所有：彦铭工作室 老陈出品
// ===============================================================================

using System;
using System.Globalization;
using System.IO;
using System.Net;
using System.Text.RegularExpressions;
using System.Web;
using Lcsoft.UrlRewriter.Actions;
using Lcsoft.UrlRewriter.Conditions;
using Lcsoft.UrlRewriter.Configuration;
using Lcsoft.UrlRewriter.Errors;
using Lcsoft.UrlRewriter.Utilities;

namespace Lcsoft.UrlRewriter
{
	/// <summary>
	/// 地址重写引擎。
	/// </summary>
	public class RewriterEngine
	{
		private const string _CONTEXT_QUERY_STRING = "HTTP_H_QUERYSTRING";
		private const string _CONTEXT_ORIGINAL_QUERY_STRING = "HTTP_H_ORIGINAL_QUERYSTRING";
		private const string _CONTEXT_RAW_URL = "HTTP_H_ORIGINAL_URL";
		private readonly RewriterConfiguration _configuration;
		private readonly IContextFacade _contextFacade;

		/// <summary>
		/// 初始化 <see cref="RewriterEngine"/> 的新实例。
		/// </summary>
		/// <param name="contextFacade">为当前请求的地址重写提供上下文信息的对象的实例。</param>
		/// <param name="configuration">地址重写配置信息。</param>
		public RewriterEngine(IContextFacade contextFacade, RewriterConfiguration configuration)
		{
			if (contextFacade == null) throw new ArgumentNullException("contextFacade");

			if (configuration == null) throw new ArgumentNullException("configuration");

			this._contextFacade = contextFacade;
			this._configuration = configuration;
		}

		/// <summary>
		/// 获取或设置原始请求地址。
		/// </summary>
		public string RawUrl
		{
			get { return (string)this._contextFacade.GetItem(_CONTEXT_RAW_URL); }
			set { this._contextFacade.SetItem(_CONTEXT_RAW_URL, value); }
		}

		/// <summary>
		/// 获取或设置当前请求包含的原始查询字符串。
		/// </summary>
		public string OriginalQueryString
		{
			get { return (string)this._contextFacade.GetItem(_CONTEXT_ORIGINAL_QUERY_STRING); }
			set { this._contextFacade.SetItem(_CONTEXT_ORIGINAL_QUERY_STRING, value); }
		}

		/// <summary>
		/// 获取或设置当前请求被重写之后的查询字符串。
		/// </summary>
		public string QueryString
		{
			get { return (string)this._contextFacade.GetItem(_CONTEXT_QUERY_STRING); }
			set { this._contextFacade.SetItem(_CONTEXT_QUERY_STRING, value); }
		}

		/// <summary>
		/// 解析地址信息。
		/// </summary>
		/// <param name="location">包含要解析的地址的 <see cref="System.String"/>。</param>
		/// <returns>返回包含解析之后的地址的 <see cref="System.String"/>。</returns>
		public string ResolveLocation(string location)
		{
			if (location == null) throw new ArgumentNullException("location");

			var appPath = this._contextFacade.GetApplicationPath();

			if (appPath.Length > 1) appPath += "/";

			return location.Replace("~/", appPath);
		}

		/// <summary>
		/// 执行地址重写。
		/// </summary>
		public void Rewrite()
		{
			var originalUrl = this._contextFacade.GetRawUrl().Replace("+", " ");
			this.RawUrl = originalUrl;

			var context = new RewriteContext(this, originalUrl, this._contextFacade.GetHttpMethod(), this._contextFacade.MapPath, this._contextFacade.GetServerVariables(), this._contextFacade.GetHeaders(), this._contextFacade.GetCookies());

			this._ProcessRules(context);
			this._AppendHeaders(context);
			this._AppendCookies(context);
			this._contextFacade.SetStatusCode((int)context.StatusCode);

			if ((context.Location != originalUrl) && ((int)context.StatusCode < 400))
			{
				if ((int)context.StatusCode < 300)
				{
					this._configuration.Logger.Info(MessageProvider.FormatString(Message.RewritingXtoY, this._contextFacade.GetRawUrl(), context.Location));
					this._HandleDefaultDocument(context); // VerifyResultExists(context);
					this._contextFacade.RewritePath(context.Location);
				}
				else
				{
					this._configuration.Logger.Info(MessageProvider.FormatString(Message.RedirectingXtoY, this._contextFacade.GetRawUrl(), context.Location));
					this._contextFacade.SetRedirectLocation(context.Location);
				}
			}
			else if ((int)context.StatusCode >= 400) this._HandleError(context);
			else if (this._HandleDefaultDocument(context)) this._contextFacade.RewritePath(context.Location);

			this._SetContextItems(context);
		}

		/// <summary>
		/// 扩展或填充所选输入。
		/// </summary>
		/// <param name="context"><see cref="RewriteContext"/> 对象的实例。</param>
		/// <param name="input">包含要处理的输入的 <see cref="System.String"/>。</param>
		/// <returns>返回包含处理之后的输入的 <see cref="System.String"/>。</returns>
		public string Expand(RewriteContext context, string input)
		{
			if (context == null) throw new ArgumentNullException("context");
			if (input == null) throw new ArgumentNullException("input");

			/* replacement :- $n
                         * |	${[a-zA-Z0-9\-]+}
                         * |	${fn( <replacement> )}
                         * |	${<replacement-or-id>:<replacement-or-value>:<replacement-or-value>}
                         * 
                         * replacement-or-id :- <replacement> | <id>
                         * replacement-or-value :- <replacement> | <value>
                         */

			/* $1 - regex replacement
			 * ${propertyname}
			 * ${map-name:value}				map-name is replacement, value is replacement
			 * ${map-name:value|default-value}	map-name is replacement, value is replacement, default-value is replacement
			 * ${fn(value)}						value is replacement
			 */

			using (var reader = new StringReader(input))
			{
				using (var writer = new StringWriter())
				{
					var ch = (char)reader.Read();

					while (ch != (char)65535)
					{
						if (ch == '$') writer.Write(this._Reduce(context, reader));
						else writer.Write(ch);

						ch = (char)reader.Read();
					}

					return writer.GetStringBuilder().ToString();
				}
			}
		}

		private void _ProcessRules(RewriteContext context)
		{
			const int maxRestart = 10; // Controls the number of restarts so we don't get into an infinite loop

			var rewriteRules = this._configuration.Rules;
			var restarts = 0;

			for (var i = 0; i < rewriteRules.Count; i++)
			{
				var condition = rewriteRules[i] as IRewriteCondition;

				if (condition != null && !condition.IsMatch(context)) continue;

				var action = rewriteRules[i] as IRewriteAction;

				if (action == null) continue;

				var processing = action.Execute(context);

				if (processing == RewriteProcessing.StopProcessing)
				{
					this._configuration.Logger.Debug(MessageProvider.FormatString(Message.StoppingBecauseOfRule));

					break;
				}

				if (processing != RewriteProcessing.RestartProcessing) continue;

				this._configuration.Logger.Debug(MessageProvider.FormatString(Message.RestartingBecauseOfRule));

				i = 0;

				if (++restarts > maxRestart) throw new InvalidOperationException(MessageProvider.FormatString(Message.TooManyRestarts));
			}
		}

		private bool _HandleDefaultDocument(RewriteContext context)
		{
			var uri = new Uri(this._contextFacade.GetRequestUrl(), context.Location);
			var b = new UriBuilder(uri);

			b.Path += "/";
			uri = b.Uri;

			if (uri.Host == this._contextFacade.GetRequestUrl().Host)
			{
				var filename = this._contextFacade.MapPath(uri.AbsolutePath);

				if (Directory.Exists(filename))
				{
					foreach (var document in RewriterConfiguration.Current.DefaultDocuments)
					{
						var pathName = Path.Combine(filename, document);

						if (!File.Exists(pathName)) continue;

						context.Location = new Uri(uri, document).AbsolutePath;

						return true;
					}
				}
			}

			return false;
		}

		//private void _VerifyResultExists(RewriteContext context)
		//{
		//    if (( String.Compare(context.Location, this.ContextFacade.GetRawUrl()) == 0 ) || ( (int) context.StatusCode >= 300 )) return;

		//    Uri uri = new Uri(this.ContextFacade.GetRequestUrl(), context.Location);

		//    if (uri.Host != this.ContextFacade.GetRequestUrl().Host) return;

		//    string filename = this.ContextFacade.MapPath(uri.AbsolutePath);

		//    if (File.Exists(filename)) this._HandleDefaultDocument(context);
		//    else
		//    {
		//        this._configuration.Logger.Debug(MessageProvider.FormatString(Message.ResultNotFound, filename));

		//        context.StatusCode = HttpStatusCode.NotFound;
		//    }
		//}

		private void _HandleError(RewriteContext context)
		{
			var statusCode = (int)context.StatusCode;

			this._contextFacade.SetStatusCode(statusCode);

			IRewriteErrorHandler handler;

			this._configuration.ErrorHandlers.TryGetValue(statusCode, out handler);

			if (handler == null) throw new HttpException(statusCode, context.StatusCode.ToString());

			try
			{
				this._configuration.Logger.Debug(MessageProvider.FormatString(Message.CallingErrorHandler));
				this._contextFacade.HandleError(handler);
			}
			catch (HttpException)
			{
				throw;
			}
			catch (Exception exc)
			{
				this._configuration.Logger.Fatal(exc.Message, exc);

				throw new HttpException((int)HttpStatusCode.InternalServerError, HttpStatusCode.InternalServerError.ToString());
			}
		}

		private void _AppendHeaders(RewriteContext context) { foreach (string headerKey in context.Headers) this._contextFacade.AppendHeader(headerKey, context.Headers[headerKey]); }

		private void _AppendCookies(RewriteContext context)
		{
			for (var i = 0; i < context.Cookies.Count; i++)
			{
				var cookie = context.Cookies[i];

				this._contextFacade.AppendCookie(cookie);
			}
		}

		private void _SetContextItems(RewriteContext context)
		{
			this.OriginalQueryString = new Uri(this._contextFacade.GetRequestUrl(), this._contextFacade.GetRawUrl()).Query.Replace("?", "");
			this.QueryString = new Uri(this._contextFacade.GetRequestUrl(), context.Location).Query.Replace("?", "");

			foreach (string key in context.Properties.Keys) this._contextFacade.SetItem(String.Format("Rewriter.{0}", key), context.Properties[key]);
		}

		private string _Reduce(RewriteContext context, TextReader reader)
		{
			string result;
			var ch = (char)reader.Read();

			if (Char.IsDigit(ch))
			{
				var num = ch.ToString(CultureInfo.InvariantCulture);

				if (Char.IsDigit((char)reader.Peek()))
				{
					ch = (char)reader.Read();
					num += ch.ToString(CultureInfo.InvariantCulture);
				}

				if (context.LastMatch != null)
				{
					var group = context.LastMatch.Groups[Convert.ToInt32(num)];

					result = group.Value;
				}
				else result = String.Empty;
			}
			else if (ch == '<')
			{
				string expr;

				using (var writer = new StringWriter())
				{
					ch = (char)reader.Read();

					while (ch != '>' && ch != (char)65535)
					{
						if (ch == '$') writer.Write(this._Reduce(context, reader));
						else writer.Write(ch);

						ch = (char)reader.Read();
					}

					expr = writer.GetStringBuilder().ToString();
				}

				if (context.LastMatch != null)
				{
					var group = context.LastMatch.Groups[expr];

					result = group != null ? group.Value : String.Empty;
				}
				else result = String.Empty;
			}
			else if (ch == '{')
			{
				string expr;
				var isMap = false;
				var isFunction = false;

				using (var writer = new StringWriter())
				{
					ch = (char)reader.Read();

					while (ch != '}' && ch != (char)65535)
					{
						if (ch == '$') writer.Write(this._Reduce(context, reader));
						else
						{
							switch (ch)
							{
								case ':':
									isMap = true;

									break;
								case '(':
									isFunction = true;

									break;
							}

							writer.Write(ch);
						}

						ch = (char)reader.Read();
					}

					expr = writer.GetStringBuilder().ToString();
				}

				if (isMap)
				{
					var match = Regex.Match(expr, @"^([^\:]+)\:([^\|]+)(\|(.+))?$");
					var mapName = match.Groups[1].Value;
					var mapArgument = match.Groups[2].Value;
					var mapDefault = match.Groups[4].Value;

					result = this._configuration.Transforms.GetTransform(mapName).ApplyTransform(mapArgument) ?? mapDefault;
				}
				else if (isFunction)
				{
					var match = Regex.Match(expr, @"^([^\(]+)\((.+)\)$");
					var functionName = match.Groups[1].Value;
					var functionArgument = match.Groups[2].Value;
					var tx = this._configuration.Transforms.GetTransform(functionName);

					result = tx != null ? tx.ApplyTransform(functionArgument) : expr;
				}
				else result = context.Properties[expr];
			}
			else result = ch.ToString(CultureInfo.InvariantCulture);

			return result;
		}
	}
}
